跟 React 学设计模式|原理篇
30 秒速记
- 核心判断:理解原理要建立输入、状态变化、输出和失败边界,而不是只记 API 与结论
- 原理主线:围绕 「高阶组件(HOC):最经典的组件逻辑复用」、「Render Props:逻辑复用的另一」、「有状态组件与无状态组件:“单一职责”原则」 建立输入、状态变化与输出之间的因果关系
- 文章范围:讲解了 React 中的高阶组件(HOC)等设计模式,介绍了其原理、实现方式及在实际开发中如何复用组件逻辑,帮助开发者更好地理解和应用 React 设计思想。
- 边界与代价:文章中的实现通常有版本和场景前提,迁移到当前项目时必须先核对运行环境与依赖
- 工程落地:面试回答按核心机制、执行链路、边界反例和工程验证展开,最后说明取舍
React 中的设计模式,本质上是在控制变化范围的同时复用组件逻辑。 HOC 通过函数包裹组件统一注入能力,render props 则把数据适配权交给调用方,因此面对不同属性名时更灵活。数据处理和界面渲染也应尽量拆到有状态组件与无状态组件中,让两类变化互不牵连。实际开发中我一般优先考虑自定义 Hook,因为它能减少嵌套和属性冲突;但维护旧类组件时,HOC 与 render props 仍有价值。
这篇文章不要按 API 清单来背。先用上面的 Mind Map 建立全局结构,再通过交互 DEMO 观察正常路径和边界路径如何改变状态;阅读正文时重点核对每一步的输入、负责执行的参与者、产生的中间状态以及最终可观察结果。遇到版本敏感结论,要把“历史实现”“当前行为”和“工程兼容策略”分开说明;遇到性能或架构取舍,则用实际指标、失败现象和验证手段支撑判断。
版本校准: 本文若分析 ReactDOM.render、旧生命周期或栈调和,应把它视为理解架构演进的历史路径。React 19 已移除 ReactDOM.render,当前客户端入口使用 createRoot;并发渲染也必须区分可中断的渲染阶段与同步提交阶段。迁移前对照 React 19 官方升级指南。
# 高阶组件(HOC):最经典的组件逻辑复用方式
# 什么是高阶组件
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。——React 官方
高阶函数的概念:接收函数作为输入,或者输出另一个函数的一类函数,就是高阶函数。
相应的,高阶组件指的就是参数为组件,返回值为新组件的函数。没错,高阶组件本质上是一个函数。下面是一个简单的高阶组件示例:
const withProps = (WrappedComponent) => {
const targetComponent = (props) => (
<div className="wrapper-container">
<WrappedComponent {...props} />
</div>
);
return targetComponent;
};
在这段代码中,withProps 就是一个高阶组件。
# 高阶组件是如何实现逻辑复用的?
现在我们考虑这样一种情况:我有一个名为 checkUserAccess 的方法,这个方法专门用来校验用户的身份是否合法,若不合法,那么一部分组件就要根据这个不合法的身份调整自身的展示逻辑(比如查看个人信息界面需要提示“请校验身份”等)。
假如说页面中的 A、B、C、D、E 五个组件都需要甄别用户身份是否合法,那么这五个组件在理论上都需要先请求一遍 checkUserAccess 这个接口。但一个一个对组件进行修改未免太麻烦了,我们期望对“获取 checkUserAccess 接口信息,并通知到对应组件”这层逻辑进行复用,这时候就可以请出高阶组件来帮忙了。
我们可以像下面代码这样在高阶组件中定义这层通用的逻辑:
// 假设 checkUserAccess 已经在 utils 文件中被封装为了一段独立的逻辑
import checkUserAccess from './utils
// 用高阶组件包裹目标组件
const withCheckAccess = (WrappedComponent) => {
// 这部分是通用的逻辑:判断用户身份是否合法
const isAccessible = checkUserAccess()
// 将 isAccessible(是否合法) 这个信息传递给目标组件
const targetComponent = (props) => (
<div className="wrapper-container">
<WrappedComponent {...props} isAccessible={isAccessible} />
</div>
);
return targetComponent;
};
这样当我们需要为某个组件复用这层请求逻辑的时候,只需要直接用 withCheckAccess 包裹这个组件就可以了。以 A 组件为例,假设 A 组件的原始版本为 AComponent,那么包裹它的形式就是下面代码这样:
const EnhancedAComponent = withCheckAccess(Acomponent);
通过简单地对高阶组件 withCheckAccess 进行引入,EnhancedAComponent 轻松具备了校验用户合法性的能力。这样一来,即便再多出 5 个组件想要引入 checkUserAccess,我们也不会怂——毕竟包裹五个组件和重写五段逻辑的工作量是没法相提并论的
高阶组件不仅能够帮助我们简化逻辑的引入过程,还可以帮助我们规避掉逻辑变更带来的烦琐的修改步骤:假如这段 checkUserAccess 的逻辑是散落在 A、B、C、D、E 这五个组件之中的,那么一旦 checkUserAccess 的判定规则需要修改,我们就得需要去修改五段代码;但现在,checkUserAccess 被抽离进了一个独立的高阶组件里,我们在高阶组件中的一次修改,将在所有被它处理过的组件中生效。
由此可以看出,高阶组件可以帮助我们从根本上减少重复的编写和修改工作,这不仅是高阶组件这一种模式的利好,更是“逻辑复用”这件事情的意义所在。
# Render Props:逻辑复用的另一种思路
术语“render prop”是指一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。——React 官方
# 什么是 render props
render props 是 React 中复用组件逻辑的另一种思路,它在实现上和高阶组件有异曲同工之妙——两者都是把通用的逻辑提取到某一处。区别主要在于使用层面,高阶组件的使用姿势是用“函数”包裹“组件”,而 render props 恰恰相反,它强调的是用“组件”包裹“函数”
一个简单的 render props 可以是这样的,见下面代码:
import React from 'react'
const RenderChildren = (props) => {
return(
<React.Fragment>
{props.children(props)}
</React.Fragment>
);
};
RenderChildren将渲染它所有的子组件。从这段代码里,你需要把握住两个要点:
render props的载体应该是一个React 组件,这一点是与高阶组件不同的(高阶组件本质是函数);render props组件正常工作的前提是它的子组件需要以函数形式存在。
第 1 点相对明显一点,你可能会对第 2 点感到迷惑。没关系,我们直接来看 RenderChildren 的使用方式,请看下面代码:
<RenderChildren>
{() => <p>我是 RenderChildren 的子组件</p>}
</RenderChildren>
RenderChildren本身是一个 React 组件,它可以包裹其他的 React 组件。一般来说,我们习惯于看到的包裹形式是“标签包裹着标签”,也就是下面代码演示的这种效果:
<RenderChildren>
<p>我是 RenderChildren 的子组件</p>
</RenderChildren>
但在 render props 这种模式下,它要求被 render props 组件标签包裹的一定是个函数,也就是所谓的“函数作为子组件传入”。这样一来,render props 组件就可以通过调用这个函数,传递 props,从而实现和目标组件的通信了。
# render props 是如何实现逻辑复用的
这里我仍然以 checkUserAccess 这个场景举例。使用 render props 复用 checkUserAccess 这段逻辑,我们可以这样做,请看下面代码:
// 假设 checkUserAccess 已经在 utils 文件中被封装为了一段独立的逻辑
import checkUserAccess from './utils
// 定义 render props 组件
const CheckAccess = (props) => {
// 这部分是通用的逻辑:判断用户身份是否合法
const isAccessible = checkUserAccess()
// 将 isAccessible(是否合法) 这个信息传递给目标组件
return <React.Fragment>
{props.children({ ...props, isAccessible })}
</React.Fragment>
};
接下来 CheckAccess 子组件就可以这样获取 isAccessible 的值,见下面代码:
<CheckAccess>
{
(props) => {
const { isAccessible } = props;
return <ChildComponent {...props} isAccessible={isAccessible} />
}
}
</CheckAccess>
到这里,“函数作为子组件传入”这种情况,我们已经了解了它的来龙去脉。但其实,
对于 render props 这种模式来说,函数并不一定要作为子组件传入,它也可以以任意属性名传入,只要 render props 组件可以感知到它就行`。
举个例子,我可以允许函数通过一个名为 checkTaget 的属性传入 render props 组件,那么 CheckAccess 组件只需要改写一下它接收函数的形式即可,见下面代码:
// 假设 checkUserAccess 已经在 utils 文件中被封装为了一段独立的逻辑
import checkUserAccess from './utils
// 定义 render props 组件
const CheckAccess = (props) => {
// 这部分是通用的逻辑:判断用户身份是否合法
const isAccessible = checkUserAccess()
// 将 isAccessible(是否合法) 这个信息传递给目标组件
return <React.Fragment>
{props.checkTaget({ ...props, isAccessible })}
</React.Fragment>
};
在使用
CheckAccess组件的时候,我们将函数放在checkTaget中传入组件即可,见下面代码:
